﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Data.Entity.Design.PluralizationServices;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
using LINQPad.Extensibility.DataContext;
using Microsoft.CSharp;

namespace NoRMLinqpadDriver
{
    /// <summary>
    /// Dynamic Linqpad Driver implementing support for the NoRM MongoDb Library
    /// Author: Oliver Weichhold (oliver@weichhold.com)
    /// </summary>
    public class Driver : DynamicDataContextDriver
    {
        #region Fields
        private static readonly PluralizationService pluralizationService = PluralizationService.CreateService(new CultureInfo("en-US"));
        private readonly MethodInfo mongoCreate;
        private readonly Type mongoType;
        private readonly Assembly normAssembly;
        private IConnectionInfo cxInfo;
        #endregion  

        public Driver()
        {
            // dynamically initialize mongo
            normAssembly = Assembly.LoadFrom(Path.Combine(GetDriverFolder(), "Norm.dll"));
            mongoType = normAssembly.GetType("Norm.Mongo");
            mongoCreate = mongoType.GetMethod("Create", new[] {typeof (string)});
        }

        #region DynamicDataContextDriver overrides

        /// <summary>Gets the name.</summary>
        public override string Name
        {
            get { return "MongoDb (NoRM)"; }
        }

        /// <summary>Gets the author.</summary>
        public override string Author
        {
            get { return "Oliver Weichhold"; }
        }

        /// <summary>Gets the connection description.</summary>
        public override string GetConnectionDescription(IConnectionInfo cxInfo)
        {
            this.cxInfo = cxInfo;
            var props = new NoRMProperties(cxInfo);

            return "MongoDb - " + Path.GetFileName(props.EntityAssemblyPath);
        }

        /// <summary>Displays the connection configuration dialog dialog.</summary>
        public override bool ShowConnectionDialog(IConnectionInfo cxInfo, bool isNewConnection)
        {
            // Prompt the user for a custom assembly and type name:
            WindowsFormsSynchronizationContext.AutoInstall = true;

            using (var cd = new DriverConfigForm(cxInfo, isNewConnection))
            {
                try
                {
                    Application.Run(cd);
                }
                catch (InvalidOperationException)
                {
                    cd.ShowDialog();
                }

                Application.Exit();
                return cd.DialogResult == DialogResult.OK;
            }
        }

        /// <summary>This virtual method is called after a data context object has been instantiated, in
        /// preparation for a query. You can use this hook to perform additional initialization work.</summary>
        public override void InitializeContext(IConnectionInfo cxInfo, object context, QueryExecutionManager executionManager)
        {
        }

        /// <summary>Gets the namespaces to add.</summary>
        public override IEnumerable<string> GetNamespacesToAdd()
        {
            return new[] {"NoRMLinqpadDriver"};
        }

        /// <summary>Gets the assemblies to add.</summary>
        public override IEnumerable<string> GetAssembliesToAdd()
        {
            var props = new NoRMProperties(cxInfo);

            return new[]
            {
                Path.Combine(GetDriverFolder(), "Norm.dll"),
                props.EntityAssemblyPath
            };
        }

        public override object[] GetContextConstructorArguments(IConnectionInfo cxInfo)
        {
            var props = new NoRMProperties(cxInfo);

            // dynamically create a Mongo database object
            object mongo = mongoCreate.Invoke(null, new object[] {props.ConnectionString});

            return new[] {mongo};
        }

        public override ParameterDescriptor[] GetContextConstructorParameters(IConnectionInfo cxInfo)
        {
            return new[] {new ParameterDescriptor("mongo", normAssembly.GetType("Norm.Mongo").FullName),};
        }

        /// <summary>
        /// Builds an assembly containing a typed data context, and returns data for the Schema Explorer.
        /// </summary>
        /// <param name="cxInfo">Connection information, as entered by the user</param>
        /// <param name="assemblyToBuild">Name and location of the target assembly to build</param>
        /// <param name="nameSpace">The suggested namespace of the typed data context. You must update this parameter if you don't use the suggested namespace.</param>
        /// <param name="typeName">The suggested type name of the typed data context. You must update this parameter if you don't use the suggested type name.</param>
        /// <returns>Schema which will be subsequently loaded into the Schema Explorer.</returns>
        public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxInfo, AssemblyName assemblyToBuild, ref string nameSpace, ref string typeName)
        {
            var props = new NoRMProperties(cxInfo);

            // load assembly
            var assembly = Assembly.LoadFrom(props.EntityAssemblyPath);

            // collect types
            string[] assemblyNamespaces = props.EntityAssemblyNamespaces.Split(';');
            var entityTypes = new List<Type>();

            for (int i = 0; i < assemblyNamespaces.Length; i++)
            {
                var filteredTypes = assembly.GetTypes().Where((x) =>
                    x.Namespace != null && x.Namespace.StartsWith(assemblyNamespaces[i].Trim())).ToList();

                entityTypes.AddRange(filteredTypes);
            }

            // Generate source
            string src = GenerateCode(entityTypes, nameSpace, typeName);

            // Build assembly
            BuildAssembly(src, props.EntityAssemblyPath, assemblyToBuild);

            // Generate schema
            return GenerateSchema(entityTypes);
        }

        #endregion

        #region Implementation
        private static List<ExplorerItem> GenerateSchema(List<Type> entityTypes)
        {
            var result = new List<ExplorerItem>();
            result.Add(GenerateTablesCategory(entityTypes));
            result.Add(GenerateCollectionsCategory(entityTypes));

            return result;
        }

        private static ExplorerItem GenerateTablesCategory(List<Type> entityTypes)
        {
            var result = new ExplorerItem("Entities", ExplorerItemKind.Category, ExplorerIcon.Schema);
            result.Children = new List<ExplorerItem>();

            for (int i = 0; i < entityTypes.Count; i++)
            {
                Type t = entityTypes[i];
                var typeItem = new ExplorerItem(Pluralize(t.Name), ExplorerItemKind.QueryableObject, ExplorerIcon.Table);
                typeItem.ToolTipText = string.Format("{0} Entity", t.Name);

                // add child nodes for properties
                PropertyInfo[] publicProperties = t.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
                typeItem.Children = new List<ExplorerItem>();

                for (int j = 0; j < publicProperties.Length; j++)
                {
                    PropertyInfo info = publicProperties[j];
                    typeItem.Children.Add(new ExplorerItem(info.Name, ExplorerItemKind.Property, ExplorerIcon.Column));
                }

                result.Children.Add(typeItem);
            }

            return result;
        }

        private static ExplorerItem GenerateCollectionsCategory(List<Type> entityTypes)
        {
            var result = new ExplorerItem("Collections", ExplorerItemKind.Category, ExplorerIcon.Schema);
            result.Children = new List<ExplorerItem>();

            for (int i = 0; i < entityTypes.Count; i++)
            {
                Type t = entityTypes[i];
                var typeItem = new ExplorerItem(Pluralize(t.Name) + "Collection", ExplorerItemKind.Property, ExplorerIcon.Schema);
                typeItem.ToolTipText = string.Format("{0} Collection", t.Name);
                result.Children.Add(typeItem);
            }

            return result;
        }

        private static string GenerateCode(IList<Type> entityTypes, string nameSpace, string typeName)
        {
            var writer = new StringWriter();

            // write namespace and opening brace 
            if (!string.IsNullOrEmpty(nameSpace))
            {
                writer.Write("namespace ");
                writer.WriteLine(nameSpace);
                writer.WriteLine("{");
            }

            // write using directives
            writer.WriteLine("using System.Linq;");
            writer.WriteLine("using Norm.Collections;");
            writer.WriteLine("");

            // write type declaration
            writer.WriteLine(string.Format("public class {0}\n{{", typeName));
            writer.WriteLine(string.Format("\tpublic {0}(Norm.IMongo mongo)\n\t{{", typeName));
            writer.WriteLine("\t\tthis.mongo = mongo;");
            writer.WriteLine("\t}\n");
            writer.WriteLine("\tprivate readonly Norm.IMongo mongo;\n");

            // write Queryable and collection properties
            for (int i = 0; i < entityTypes.Count(); i++)
            {
                var type = entityTypes[i];
                var name = Pluralize(type.Name);

                writer.WriteLine(string.Format(
                    "\tpublic IQueryable<{0}> {1} {{ get {{ return this.mongo.GetCollection<{0}>().AsQueryable(); }} }}\n", type.FullName, name));

                writer.WriteLine(string.Format(
                    "\tpublic IMongoCollection<{0}> {1} {{ get {{ return this.mongo.GetCollection<{0}>(); }} }}\n", type.FullName, name + "Collection"));
            }

            // write type closing brace 
            writer.WriteLine("}");

            // write namespace closing brace 
            if (!string.IsNullOrEmpty(nameSpace))
                writer.WriteLine("}");

            return writer.ToString();
        }

        private static string Pluralize(string name)
        {
            return pluralizationService.Pluralize(name);
        }

        private void BuildAssembly(string code, string entityAssembly, AssemblyName name)
        {
            // Use the CSharpCodeProvider to compile the generated code:
            CompilerResults results;

            var assemblyNames = new List<string>();
            assemblyNames.AddRange("System.dll System.Core.dll".Split());
            assemblyNames.Add(entityAssembly);
            assemblyNames.Add(Path.Combine(GetDriverFolder(), "Norm.dll"));

            using (var codeProvider = new CSharpCodeProvider(new Dictionary<string, string> {{"CompilerVersion", "v4.0"}}))
            {
                var options = new CompilerParameters(assemblyNames.ToArray(), name.CodeBase, true);
                results = codeProvider.CompileAssemblyFromSource(options, code);
            }

            if (results.Errors.Count > 0)
                throw new Exception("Cannot compile typed context: " + results.Errors[0].ErrorText + " (line " + results.Errors[0].Line + ")");
        }

        #endregion
    }
}